机器人的「语料」,如何获取?
本文来自作者 李烨 在 GitChat 上分享「应用聚类模型获得聊天机器人语料」,「阅读原文」查看交流实录
「文末高能」
编辑 | 嘉仔
0. 聊天机器人系列第三部
之前笔者开过两个关于聊天机器人开发的 Chat:《从零开始,开发一款聊天机器人》,和《聊天机器人语言理解模块开发实践》。
前者讲述开发一款聊天机器人的过程;后者则细述了在不利用现有工具的情况下,实现语言理解模型的方法——对意图识别和实体抽取模型的原理和算法步骤进行了说明。
一个模型光有算法不行,还必须有数据。本 chat 就专门来讲一讲,如何获取聊天机器人语言理解模型的训练数据,尤其是,如何利用已有的积累获取有效训练数据。
同时,本 chat 也用来向大家展示一下:如何在工程实践中利用学术研究的成果。
本次 chat 描述的过程,是一个在人工智能工程类工作中非常典型的实例:通过阅读论文,实践论文内容来解决实际问题。
1. 从用户日志中挖掘训练语料
1.1 语言理解模型的语料
所谓语料(Utterance),就是语言理解模型的训练数据。因为意图识别和实体抽取都是有监督模型,因此他们所需要的训练数据都是标注数据。在数据被标注之前,我们称其为原始语料。
聊天机器人语言理解模型所需要的原始语料,是人类对聊天机器人说的话(query),这些query在经过标注后成为训练语料。
原始语料一般有两个来源:
i)人工生成——在毫无积累的情况下,只能人为编写,或者借助部分手段(e.g. 基于规则的语言片段替换等)半自动生成。
ii)从用户日志中选取——有些聊天机器人所对应的场景,早已经有人类客服为之工作。
在这种情况下,用户日志中就会记载大量之前用户和人类客服之间的对话。这些对话,都有可能用来作为训练机器人语言理解的语料。
1.2 语料对标注的影响
在《从零开始,开发一款聊天机器人》中,我们专门讲过语料标注的方法。
意图识别是分类模型,因此,每一条用于训练的语料都有一个标签,这个标签就是最终该语料被判定的意图。
在进行标注数据的时候,总共有哪些意图(标签),都已经确定了。所有的意图是一个有限集合,且理论上在标注过程中不应变动(虽然在实际操作中变动时常发生,但如果发生变动,已标注数据都需要被重新标注)。
这个意图集合是如何产生的?当然是开发者定义的。开发者可以完全凭空定义吗?当然不是!
一个聊天机器人的意图,直接影响了该机器人在获取用户query后可能产生的回复。因此,意图定义必须和这个聊天机器人在真实应用中要回答的用户问题存在直接的关联。
例如:一个电商的客服机器人经常要处理用户关于邮寄/快递费用的询问,自然,在训练它的LU模型时,就应定义与之对应的意图。
一个培训学校的客服需要经常提供选课服务,它也应有相应意图。反过来,电商客服不需要“选择课程”意图,培训客服一般也不需要“减免邮费”意图。
有些意图我们可以通过已经掌握的领域知识来直接定义,比如:只要是电商客服,一定需要“查询商品”意图。
但是,作为程序员,我们不太可能对所有客户的业务领域了如指掌,因此也就不能保证自己拍脑袋想出来的意图集合完备且必要。
在这种情况下,通过对用户日志的分析能够帮我们了解用户的日常业务,并进一步为如何定义意图及实体提供意见。
1.3 分析用户日志
拿到用户日志的第一步工作是:阅读——通过阅读既往客服和用户之间交流的记录来了解场景、业务,必不可少。
但是要客观的分析用户数据,对其有一个整体的了解和掌握,仅仅靠直观感性认识还是不够的。我们还需要采取一些数据分析的手段。
如果我们知道用户的问题大致能够分为那些“类”,必然对之后定义意图的 38 39760 38 15232 0 0 1449 0 0:00:27 0:00:10 0:00:17 2882作有极大帮助。但是,此时我们偏偏不知道如何对日志语料进行*分类*操作。我们能用的方法是:聚类。
所谓聚类,指一系列机器学习算法,它们能够把内容相近的语料放到一起,形成一个“簇”。
这里面的“簇”,都不是预先定义的,而是依据各自算法的区分原则,对数据做处理后自然形成的结果。
现在,我们要做的,就是对用户日志中的用户query进行聚类。
2. 对用户日志语料进行聚类
2.1 聚类算法选取
说到聚类,最常见的模型当然是 KMeans。
不过如果使用 KMeans 的话,需要指定K的值,也就是要在训练前指定最后的结果被分为几“簇”。当我们分析某客户的用户日志时,并不知道K是几。K是我们希望通过聚类发现的。
我们还希望聚类结果形成的若干簇,大小不要差异太大(如果结果是一个簇包含了90%的语料。
另外若干簇分担10%,那么显然对我们定义意图没有太大帮助)。而KMeans的结果并不能保证每簇中的个体数量低于某个量值。
因此,KMeans并不符合当前任务的要求。
那应该用什么模型呢?经过一通搜索调研,以及请教公司内部的数据科学家,最终确定了聚类算法:基于图切割的谱聚类(Spectral Clustering)。
2.2 根据论文实现算法
通过搜索,笔者发现一篇论文:《A Tutorial on Spectral Clustering》( http://www.kyb.mpg.de/fileadmin/user_upload/files/publications/attachments/luxburg06_TR_v2_4139%5B1%5D.pdf ) ,其中非常详细的讲述了谱聚类算法的步骤,并且有严格的推导公式。
笔者于是决定,以本论文为基础依据,同时结合其他同事在图切割方法上的实践经验,自己编码实现谱聚类算法。
2.3 聚类算法步骤
根据论文和专家意见,经过一番尝试,最终按照以下步骤实现了这个聚类程序:
【步骤1】 将每个 Utterance 转化为一个词袋,并经过 normalization 和去停用词操作。
【步骤2】生成一张图 G = < V,E >,每个词袋是其中一个顶点(vertex),每两个顶点之间的边则是这两个词袋之间的 Jaccard 距离:
Jaccard_Distance = 1 – (|A ∩ B|/ |A U B|)
这样一张图用矩阵 C = (cij) 存储; 其中:
cij =Jaccard_Distance(wordbag_i, wordbag_j)
【步骤3】确定距离阈值 threshold_C, 将所有cij > threshold_C 的两个顶点 i 和 j 视为断开。据此将完整的G分割为若干连通图{G1, G2, … , Gn}。
计算每一个子图的 radius (最远边缘节点到中心节点的距离) 和 size(包含顶点数)。
如果(cluster_radius<= threshold_radius) && (cluster_size <= threshold_size)
, 则该连通图本身就是一个独立的簇,否则,对该簇进行下一个步骤。
【步骤4】图切割:
4-1 将待切割图G切割为2个子图Gs1 和Gs2, 达到Gs1和Gs2之间距离尽量大而两个子图内部个节点间距离尽量小的目的。具体切割过程如下:
4-1-1 构造一个和C同等大小的矩阵W = (wij)
wij = exp(-||xi - xj||2/σ2) ; 这里用到了高斯相似度函数(Gaussian similarity function),其中σ 是一个用来控制变换速度的参数。
具体到本例子, ||xi-xj||就用 wordbag_i 和 wordbag_j 的 Jaccard_distance 来计算即可。也就是说 ||xi-xj|| = cij。
4-1-2 构造一个对角矩阵D:
对角项 di = Σj wij
4-1-3 令 L = D - W,构造等式:
Lv = (D-W)v = λv,其中λ为常数,v为向量。
4-1-4 计算上述等式的倒数第二小的特征值所对应特征向量f。
设被分割图 G一共包含n个顶点,则其由一个nxn矩阵表达,因此得出的特征向量f也是n维向量。f中的每一维代表一个顶点(即一个Utterance)。
f = (f1, f2, …, fn);
IF fi >= 0 THEN vertex_i 属于 Gs1 IF fi < 0 THEN vetex_i 属于 Gs2
这样就把G分成了两部分:Gs1和Gs2。
4-2 计算Gs1和Gs2的大小和半径,然后
IF ((cluster_radius > threshold_radius) && (cluster_size > threshold_size)) THEN 重复 4-1,直到所有被分割的结果满足上述条件为止。
【步骤5】将【步骤4】运用到所有【步骤3】中所有连通图{G1, G2, … , Gn}上。
3. 工程实践
3.1 编程语言和运行结果
因为当时所处理的日志是记录在 PostgresDB 中的结构化数据,要用一种能够方便操作数据库的语言。
因此谱聚类程序的第一个版本是用内嵌R的 PostgreSQL 编写的。
总共40多万条(平均每条不超过1K),用SQL+R也并不是很慢。
R的矩阵运算功能使得整个程序编写相当容易,将threshold_size
设为2000,threshold_radius
设为50之后,整体运行结果还不错。最后得到了17个簇,人工看结果也比较reasonable。
虽然最终的意图定义并非和这些分出来的簇一一对应,但是经过反复调参得到的聚类结果,包括整个调参过程,都让我们对聊天机器人的应用场景有了进一步深入的了解。
3.2 被质疑的算法
不过在团队内部分享算法实现的时候,有同事说:这不就是算词频嘛。态度颇为不屑。
当时的感觉是有些无言以对。
词袋模型(用词袋来表示Utterance)本身确实是没有考虑单词出现顺序对文档含义的影响,而且采用 Jaccard Distance 计算的是两个词袋之间单词交集和并集的比例,也确实和词语出现的次数相关。
但是如果说谱聚类就是算词频,则相差太远了。
可是具体差在哪里呢?好像又有点说不清楚。只是朦胧的觉得,如果不是采用了相似度矩阵,如果不是进行了矩阵运算,则根本得不出“簇”的结果——
要算单个文档(Utterance)的词频或者总体词频都很容易。但是如果不是矩阵运算,又怎么可能把这些文档归为若干簇,使得每一个簇内文档相互之间的距离尽量小,而簇之间的距离尽量大呢?
模模糊糊有如上这样的想法。但是,其实自己并不明白,即使采用了谱聚类,又是怎么能做到不同的簇高内聚低耦合的。
还有,为什么要做那么一大堆奇怪的矩阵变换?为什么要把一个好好的相似度矩阵扭曲成很奇怪的样子?这样做到底是为什么呢?
凭直觉猜想肯定是不行的,要面对质疑,就不能只停留在依据步骤实现算法,必须从理论层面彻底掌握算法的原理及数学推导过程!
笔者回头从理论层面一步步推导谱聚类的数学原理,从头到尾理清之后,才对上述的疑问有了解答。
4 掌握原理,面对质疑
4.1 基本原理
谱聚类的目的就是要找到一种合理的分割,使得分割后形成若干子图,连接不同的子图的边的权重尽可能低,即“截”最小,同一子图内的边的权重尽可能高。
4.2 推导过程
具体过程是经历了以下这些操作,实现的:
4.2.1 构造矩阵
步骤 4-1-1 中根据对称阵C构造的矩阵W,也是一个对称阵。它描述了G中各节点间的相似度。
NOTE 1: 在步骤2中构造的矩阵 C = (cij) 中,cij表示点 i 到点 j 的距离,cij值越大说明距离越远。但是到了矩阵W中的节点:wij = exp(-(cij)2/σ2)。cij 越大,则wij越小。也就是说W中的节点wij 的数值越小,它所表示的对应的两个点之间的距离也就越大。
步骤 4-1-2 则是构造了W的对角矩阵D。
步骤 4-1-3 中,由相似度矩阵W和其对角矩阵D,我们构造了一个新的矩阵:
L= D-W
L是一个拉普拉斯(Laplacian)矩阵,称作非规范化的拉普拉斯矩阵(Unnormalized Laplacian Matrix)。
4.2.2 拉普拉斯矩阵性质
因拉普拉斯矩阵性质得知:
(i) L是对称半正定矩阵;
(ii) Laplacian矩阵L的最小特征值是0,相应的特征向量是I;
(iii) Laplacian矩阵L有n个非负实特征值:0 = λ1 <= λ2 <= … <= λn
又因为L = D - W,对于任一实向量f,都可以做如下计算:
因此,对于任一实向量f都有下面的式子成立:
4.2.3 图分割和矩阵运算的关系
现在我们回过头来,看图切割这件事情。
假设我们把L所对应的原图进行图切割,成两个新的图:A和
也就是说,之前n x n矩阵L所对应的n个顶点被分为了两部分,一部分属于A,另一部分属于它的补
到底哪些点被分给了A,哪些点被分给了它的补呢?我们可以用一个向量来表示。
假设存在一个向量f = (f1, f2, …, fn)’, 其中不同维度的值可以指示该维度对应的顶点属于新分出来的哪个子图。具体如下:
将f带入到上面(iii)中的公式:
因此,f’Lf 就可以被转化为如下形式:
<式子1>
取出上面<式子1>中的后一部分:
其中:k表示不同类别的个数,这里k=2。
这里的定义的Cut函数,又可以被称为“截函数”。
当一个图被划分成为两个子图时,“截”即指子图间的连接密度。即被切割后的子图之间,原本是连通状态(但在切割时被截断)的边的值加权和。
我们要找到一种分割,使得分割后,连接被分割出来的两个子图的边的权重尽可能低,即“截”最小。
因此,Cut函数就是我们求取图切割方法的目标函数。
因为在这个例子里面,Cut 函数中的值是就是 wij (顶点i 位于A, 顶点 j 位于
我们既然要让切割出来的结果是两个子图之间的加权距离尽量大,那么自然,我们就要让
由此可知,我们要做的就是最小化Cut函数。
我们再将Cut函数带回到<式子1>中,得到结果如下:
其中|V|表示的是顶点的数目,对于确定的图来说是个常数。
由上述的推导可知,由f’Lf推导出了RatioCut函数。到此,我们得出了:
因为 Cut 函数和 RatioCut 函数相差的是一个常数,因此求Cut的最小值就是求 RatioCut 的最小值。
又因为|V|是常数,因此我们求 RatioCut 函数的最小值就是求f’Lf的最小值。
到此时,图切割问题,就变成了求 f’Lf 的最小值的问题。
4.2.4 通过求f’Lf的最小值来切割图
假设 λ 是 Laplacian 矩阵L的特征值,f是特征值λ对应的特征向量,则有:Lf = λf
在上式的两端同时左乘f’,得到:
f’Lf = λf’f
已知 ||f|| = n1/2 则 f’f = n,上式可以转化为:f’Lf = λn
既然我们的目标是求
由 Laplacian 矩阵的性质可知,Laplacian 矩阵的最小特征值为0,相应的特征向量是I。
向量I中所有维度 都为1,无法将对应顶点分为两份。因此我们用L第二小的特征值(也就是最小的非零特征值)来近似取 RatioCut 的最小值。(本自然段描述纯为笔者的理解,此处背后实际的理论依据是 Rayleigh-Ritz 理论。)
我们先求出L第二小的特征矩阵f,再通过如下变换,将f转化为一个离散的指示向量:
对于求解出来的特征向量f = (f1,f2,…, fn)’ 中的每一个分量fi,根据每个分量的值来判断对应的点所属的类别:
这也就是步骤4-1-4中描述的内容。
4.2.5 从切割成2份到切割成k份的推演
如果不是要一次将图切成2份,而是要切成k份,那么就要先求L的前k小个特征向量。
若前k个特征向量为
将特征向量矩阵中的每一行作为一个样本,利用K-Means聚类方法对其进行聚类。
即对n个k维向量进行聚类,将其聚为k个簇。聚类完成之后,如果特征矩阵中的第i个k维向量被聚集到了第j个簇中,则原本图中的第i个点就被聚集到了第j个簇中。
以上,就是根据非规范化拉普拉矩阵进行基于图切割的谱聚类的算法原理。
4.2.6 规范化拉普拉斯矩阵
L 也可以被规范化,D-1/2L D-1/2 就是L的规范化形式。
L’ = D-1/2L D-1/2 又称为规范化的拉普拉斯矩阵(Normalized Laplacian Matrix)。
对于规范化的拉普拉斯矩阵,不能直接求其特征值和特征向量来做图切割。不过大体过程和思路与非规范化拉普拉斯矩阵一致,在此不赘述。
5. 筛选出训练语料
当然,谱聚类仅是分析用户日志的工具之一。除此之外,还有许多手段可以用来帮助我们了解数据。
比如,我们可以:
用 word2vec 训练词向量模型;然后再针对每条语料生成词袋模型,通过拼接词袋中各词词向量的方式生成词袋向量;
再计算不同词袋向量之间的相似度;设定相似度阈值,来将所有语料分为若干“簇”。
根据一些基于先验知识的简单规则,对语料做一次基于规则的“粗分类”;然后计算各语料中非停用词的tf-idf和信息熵,以信息熵为依据进行聚类。
应用LDA模型进行聚类。
等等……
以上各种方法可以组合或叠加使用,包括其中各种将自然语言语料转化为向量空间模型的方式,也可以替换或者组合使用。
笔者实践了谱聚类和上面列出的三种聚类方法,最终实际的体会:还是谱聚类的效果最好。这可能和聊天机器人语料的客观特点有关——用户query 一般比较简短,不属于长文档;
不同意图的分布又不甚平均;有些意图之间的区分度也不是非常明显;加之中文语料,需要先进行分词处理,分词程序的质量也直接影响着最终效果。
种种原因导致最终谱聚类胜出,为最终定义意图、实体和筛选语料做出了贡献。
因为考虑到数据标注的人力成本,虽然有几十万条备选语料,我们最终还是采用简单规则+随机的方法,从中抽取了几千条进行标注,最终生成了语言理解模型的训练数据。
近期热文
从 TensorFlow 开始学习人工智能
「阅读原文」看交流实录,你想知道的都在这里